package com.upb.webserver.application;

import cn.hutool.core.collection.CollectionUtil;
import com.upb.webserver.account.AccountService;
import com.upb.webserver.applicationorg.ApplicationOrgService;
import com.upb.webserver.chain.ChainManager;
import com.upb.webserver.common.constant.Constant;
import com.upb.webserver.common.enums.*;
import com.upb.webserver.common.exception.WebServerException;
import com.upb.webserver.common.pojo.application.RspApplicationVO;
import com.upb.webserver.common.pojo.base.BasePageResponse;
import com.upb.webserver.common.pojo.order.ApplicationUnionOrder;
import com.upb.webserver.common.pojo.order.ApplicationUnionOrderParam;
import com.upb.webserver.common.pojo.order.ReqAddWorkOrderOfNewApplicationVO;
import com.upb.webserver.common.pojo.rest.fisco.chainmgr.resp.RspSimpleNodeInfoVo;
import com.upb.webserver.common.pojo.rest.fisco.data.resp.GroupInfo;
import com.upb.webserver.common.returncode.ConstantCode;
import com.upb.webserver.common.tools.JsonUtils;
import com.upb.webserver.company.CompanyDeveloperManager;
import com.upb.webserver.company.CompanyDeveloperService;
import com.upb.webserver.dao.entity.*;
import com.upb.webserver.dao.mapper.*;
import com.upb.webserver.developerappmap.DeveloperAppMapService;
import com.upb.webserver.message.MessageService;
import com.upb.webserver.order.OrderService;
import com.upb.webserver.order.WorkOrderManager;
import com.upb.webserver.org.OrgManager;
import com.upb.webserver.rest.fisco.FiscoChainMgrRestService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.SetUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.stream.Collectors;

import static com.upb.webserver.common.constant.Constant.*;

@Slf4j
@Service
public class ApplicationService {
    @Autowired
    private AccountService accountService;
    @Autowired
    private ApplicationManager applicationManager;
    @Autowired
    private TbApplicationOrgMapper applicationOrgMapper;
    @Autowired
    private TbApplicationMapper applicationMapper;
    @Autowired
    private OrderService orderService;
    @Autowired
    private WorkOrderManager workOrderManager;
    @Autowired
    private OrgManager orgManager;
    @Autowired
    private MessageService messageService;
    @Autowired
    private CompanyDeveloperService companyDeveloperService;
    @Autowired
    private CompanyDeveloperManager companyDeveloperManager;
    @Autowired
    private TbWorkOrderMapper orderMapper;
    @Autowired
    private TbDeveloperMapper developerMapper;
    @Autowired
    private TbOrgMapper orgMapper;
    @Autowired
    private ChainManager chainManager;
    @Autowired
    private FiscoChainMgrRestService fiscoChainMgrRestService;
    @Autowired
    private ApplicationOrgService applicationOrgService;
    @Autowired
    private DeveloperAppMapService developerAppMapService;


    @Transactional
    public TbApplication saveNewApplication(Byte platform, int creatorId, ReqAddWorkOrderOfNewApplicationVO newApplicationVO) {
        log.info("start exec method [saveNewApplication]. newApplicationVO:{}", JsonUtils.objToString(newApplicationVO));

        TbChain tbChain = chainManager.verifyChainId(newApplicationVO.getChainId());

        //check record
        TbApplication application = applicationMapper.selectByChainAndName(tbChain.getId(), newApplicationVO.getName());

        log.info("query app by chain:{} and name:{}, the result:{}", newApplicationVO.getChainId(), newApplicationVO.getName(), JsonUtils.objToString(application));

        //add
        if (Objects.isNull(application)) {
            //save application
            TbApplication newApp = new TbApplication();
            BeanUtils.copyProperties(newApplicationVO, newApp);
            newApp.setChainType(tbChain.getChainType());
            newApp.setChainId(tbChain.getId());
            newApp.setCreateFromPlatform(platform);
            newApp.setCreateBy(creatorId);
            newApp.setCreateStatus(ChainOrAppStatusEnum.ALREADY_TO_INITIALIZED.getId());
            newApp.setGmtCreate(new Date());
            newApp.setGmtModified(new Date());
            applicationMapper.insertSelective(newApp);

            //add finalName
            TbApplication tbApplication = applicationMapper.selectByPrimaryKey(newApp.getId());
            String appFinalName = String.format(APPLICATION_FINAL_NAME_FORMAT, newApp.getId());
            tbApplication.setFinalName(appFinalName);
            applicationMapper.updateByPrimaryKey(tbApplication);

            log.info("success exec method [saveNewApplication]. tbApplication:{}", JsonUtils.objToString(tbApplication));
            return tbApplication;
        }

        //update
        if (application.getCreateStatus() == ChainOrAppStatusEnum.DEPLOY_FAIL.getId()) {
            log.info("application:{} already exist,and old status is:{},update new status to:{}", newApplicationVO.getName(), application.getCreateStatus(), ChainOrAppStatusEnum.ALREADY_TO_INITIALIZED.getId());
            application.setCreateStatus(ChainOrAppStatusEnum.ALREADY_TO_INITIALIZED.getId());
            application.setGmtModified(new Date());
            applicationMapper.updateByPrimaryKey(application);
            return application;
        }

        log.warn("fail exec method [saveNewApplication]. found record by chainId:{} and applicationName:{} ,   and the application status is:{}", newApplicationVO.getChainId(), newApplicationVO.getName(), application.getCreateStatus());
        throw new WebServerException(ConstantCode.NOT_SUPPORT_OPERATE);
    }


    /**
     * getChainInfoList.
     *
     * @return
     */
    public List<GroupInfo> getGroupList(Byte chainType) {
        List<GroupInfo> list = applicationMapper.getGroupList(chainType);
        return list;
    }


    public BasePageResponse queryApplicationByPage(Byte platform, int currentAccountId, String currentAccountRole, Integer chainId, Set<Byte> inputAppStatusSet, int pageNumber, int pageSize) {
        log.info("start exec method [queryApplicationByPage]. platform:{} currentAccountId:{}  currentAccountRole:{} pageNumber:{} pageSize:{} chainId:{} inputAppStatusSet:{}", platform, currentAccountId, currentAccountRole, pageNumber, pageSize, chainId, JsonUtils.objToString(inputAppStatusSet));

        //param of application union workOrder
        ApplicationUnionOrderParam param = initParamOfApplicationPage(platform, currentAccountId, currentAccountRole, chainId, inputAppStatusSet, pageNumber, pageSize);

        //query count
        BasePageResponse pageResponse = BasePageResponse.initialize();
        long applicationCount;
        if ((applicationCount = orderMapper.unionQueryCountFromApplicationAndWorkOrder(param)) == 0) {
            log.info("finish exec method [queryApplicationByPage], result:{}", JsonUtils.objToString(pageResponse));
            return pageResponse;
        }

        //query by param
        List<ApplicationUnionOrder> appUnionOrderList = orderMapper.unionQueryFromApplicationAndWorkOrder(param);
        Set<Integer> appIdSet = appUnionOrderList.stream().map(c -> c.getApplicationId()).filter(applicationId -> applicationId != null).collect(Collectors.toSet());
        List<TbApplication> applicationList = applicationManager.getAllApplicationByIdSet(appIdSet);

        List<RspApplicationVO> applicationVOList = new ArrayList<>();
        for (TbApplication application : applicationList) {
            RspApplicationVO rspVo = new RspApplicationVO();
            BeanUtils.copyProperties(application, rspVo);
            rspVo.setRemoteId(ChainTypeEnum.FISCO.getId() == application.getChainType() ? String.valueOf(application.getId()) : application.getFinalName());
            rspVo.setCreatorAccountName(accountService.getAccountNameById(application.getCreateFromPlatform(), application.getCreateBy()));
            rspVo.setCreatorOrg(applicationOrgService.getCreatorOrgName(application.getCreateFromPlatform(), application.getCreateBy(), application.getId()));
            applicationVOList.add(rspVo);
        }


        //query by workOrderId
        Set<Integer> orderIdSet = appUnionOrderList.stream().map(c -> c.getWorkOrderId()).filter(orderId -> orderId != null).collect(Collectors.toSet());
        List<TbWorkOrder> workOrderList = workOrderManager.getAllWorkOrderByIdSet(orderIdSet);
        for (TbWorkOrder workOrder : workOrderList) {
            RspApplicationVO applicationVO = buildRspFabricAppVO(platform, currentAccountId, workOrder);
            if (Objects.isNull(applicationVO)) {
                continue;
            }
            applicationVOList.add(applicationVO);
        }


        //set properties name by id
        applicationVOList.stream().forEach(app -> {
            TbChain tbChain = chainManager.verifyChainId(app.getChainId());
            app.setChainName(tbChain.getName());
            app.setEncryptionId(tbChain.getEncryptionId());
        });

        pageResponse.setTotalCount(applicationCount);
        pageResponse.setData(applicationVOList.stream().sorted().collect(Collectors.toList()));
        log.info("start exec method [queryApplicationByPage]. platform:{} currentAccountId:{} pageNumber:{} pageSize:{} inputChainStatusSet:{} result:{}", platform, currentAccountId, pageNumber, pageSize, JsonUtils.objToString(inputAppStatusSet), JsonUtils.objToString(pageResponse));
        return pageResponse;
    }


    /**
     * @param platform
     * @param currentAccountId
     * @param inputAppStatusSet
     * @param pageNumber
     * @param pageSize
     * @return
     */
    public ApplicationUnionOrderParam initParamOfApplicationPage(Byte platform, int currentAccountId, String currentAccountRole, Integer chainId, Set<Byte> inputAppStatusSet, int pageNumber, int pageSize) {
        log.info("start exec method [initParamOfApplicationPage]. platform:{} currentAccountId:{} currentAccountRole:{} pageNumber:{} pageSize:{} chainId:{} inputAppStatusSet:{}", platform, currentAccountId, currentAccountRole, pageNumber, pageSize, chainId, JsonUtils.objToString(inputAppStatusSet));

        //status of chain or workOrder
        Set<Byte> applicationStatusSetOnDb = new HashSet<>();
        Set<Byte> workOrderStatusSetOnDb = new HashSet<>();
        for (Byte inputAppStatus : inputAppStatusSet) {
            if (Constant.CHAIN_OR_APP_STATUS_DEPLOY_SUCCESS.contains(inputAppStatus)) {
                applicationStatusSetOnDb.add(inputAppStatus);
            } else {
                ChainOrAppStatusEnum chainOrAppStatusEnum = ChainOrAppStatusEnum.getById(inputAppStatus);
                if (null == chainOrAppStatusEnum) {
                    throw new WebServerException(ConstantCode.PARAM_EXCEPTION.attach(String.format("invalid inputAppStatus:%d", inputAppStatus)));
                }
                workOrderStatusSetOnDb.add(chainOrAppStatusEnum.convertToOrderStatusEnum().getValue());
            }
        }


        //add all workOrderId by chainId
        Set<Integer> workOrderIdSet = new HashSet<>();
        if (chainId != null) {
            Set<Integer> workOrderIdRecordByChainId = workOrderManager.getAllWorkOrderOfNewApplicationByChainId(chainId);
            workOrderIdSet.addAll(workOrderIdRecordByChainId);
        }

        //all workOrderId and applicationId about developer
        Set<Integer> applicationIdSet = new HashSet<>();
        if (PlatformEnum.DEVELOPER.getId() == platform || RoleTypeEnum.API_MANAGER.getName().equalsIgnoreCase(currentAccountRole)) {
            List<Integer> companyIdList = null;
            if (RoleTypeEnum.API_MANAGER.getName().equalsIgnoreCase(currentAccountRole)) {
                companyIdList = companyDeveloperManager.listCompanyIdByPlatformAndAccountId(platform, currentAccountId);
            }

            if (PlatformEnum.DEVELOPER.getId() == platform) {
                int companyId = companyDeveloperService.selectCompanyIdByDeveloperId(currentAccountId);
                companyIdList = Arrays.asList(companyId);
            }

            workOrderIdSet = workOrderManager.getAllWorkOrderOfNewApplicationByCompanyIdList(companyIdList);

            if (CollectionUtil.isEmpty(workOrderIdSet)) {
                workOrderIdSet = CollectionUtil.newHashSet(Integer.MIN_VALUE);
            }

            applicationIdSet = applicationManager.getApplicationIdSetByCompanyIdList(companyIdList);
            if (CollectionUtil.isEmpty(applicationIdSet)) {
                applicationIdSet = CollectionUtil.newHashSet(Integer.MIN_VALUE);
            }
        }


        //param of chain union workOrder
        ApplicationUnionOrderParam param = new ApplicationUnionOrderParam();
        param.setApplicationStatusSet(applicationStatusSetOnDb);
        param.setWorkOrderStatusSet(workOrderStatusSetOnDb);
        param.setWorkOrderIdSet(workOrderIdSet);
        param.setApplicationIdSet(applicationIdSet);
        param.setPageNumber(pageNumber);
        param.setPageSize(pageSize);
        param.setSortTypeEnum(MysqlSortTypeEnum.DESC);
        param.setChainId(chainId);

        log.info("success exec method [initParamOfApplicationPage]. result:{}", JsonUtils.objToString(param));
        return param;
    }


    /**
     * @param platform
     * @param currentAccountId
     * @param workOrder
     * @return
     */
    private RspApplicationVO buildRspFabricAppVO(int platform, int currentAccountId, TbWorkOrder workOrder) {

        if (null == workOrder) {
            log.warn("fail exec method[buildRspChainVO]. workOrder is null");
            return null;
        }

        ReqAddWorkOrderOfNewApplicationVO appDetailVO = JsonUtils.objToJavaBean(workOrder.getDetail(), ReqAddWorkOrderOfNewApplicationVO.class);

        TbChain tbChain = chainManager.verifyChainId(appDetailVO.getChainId());

        RspApplicationVO tbApp = new RspApplicationVO();
        BeanUtils.copyProperties(workOrder, tbApp);
        BeanUtils.copyProperties(appDetailVO, tbApp);
        tbApp.setWorkOrderId(workOrder.getId());
        tbApp.setId(workOrder.getTargetId());
        tbApp.setChainType(tbChain.getChainType());
        tbApp.setCreatorAccountName(accountService.getAccountNameById(workOrder.getCreateFromPlatform(), workOrder.getCreateBy()));
        tbApp.setCreatorOrg(orderService.getCreatorOrgName(workOrder));
        tbApp.setHadSelfCompleted(orderService.getDeveloperCompleteStatus(platform, currentAccountId, workOrder));

//        tbApp.setCreateDeveloperId(workOrder.getCreateBy());
//        tbApp.setCreateCompanyId(companyDeveloperService.selectCompanyIdByDeveloperId(workOrder.getCreateBy()));
//        Integer createOrgId = orderService.getCreaterOrg(workOrder);
//        if (Objects.nonNull(createOrgId)) {
//            tbApp.setCreateOrgId(createOrgId);
//        } else {
//            log.warn("not found createOrg, workOrder:{}", JsonUtils.objToString(workOrder));
//        }
        ChainOrAppStatusEnum chainOrAppStatusEnum = OrderStatusEnum.getByValue(workOrder.getStatus()).convertToChainOrAppStatusEnum();
        if (chainOrAppStatusEnum != null) {
            tbApp.setCreateStatus(chainOrAppStatusEnum.getId());
        }

        return tbApp;
    }


    @Transactional
    public void joinOrganization(int applicationId, int orgId) {
        log.info("start exec method [joinOrganization]. applicationId:{} orgId:{}", applicationId, orgId);
        //verify appId
        TbApplication tbApplication = applicationManager.verifyApplicationId(applicationId);

        //add developer-app map
        TbOrg tbOrg = orgManager.verifyOrgId(orgId);
        developerAppMapService.addByApplicationSetAndCompanySet(CollectionUtil.newHashSet(tbApplication.getId()), SetUtils.hashSet(tbOrg.getCompanyId()));


        if (ChainTypeEnum.FISCO.getId() == tbApplication.getChainType()) {
            joinFiscoOrg(tbApplication, orgId);
        } else {
            log.warn("not support chainType:{}", tbApplication.getChainType());
            throw new WebServerException(ConstantCode.CHAIN_TYPE_NOT_SUPPORT);
        }

    }

    @Transactional
    public void joinFiscoOrg(TbApplication tbApplication, int orgId) {
        //query node list by app and org
        String nodeTypes = String.join(",", NODE_TYPE_OBSERVER, NODE_TYPE_REMOVE);
        List<RspSimpleNodeInfoVo> nodeList = fiscoChainMgrRestService.selectNodeIdList(tbApplication.getChainId(), tbApplication.getId(), orgId, nodeTypes);
        if (CollectionUtil.isEmpty(nodeList))
            throw new WebServerException(ConstantCode.NODE_FOUND_NODE_OF_OBSERVER_OR_REMOVE);

        // save application-org
        applicationOrgService.saveApplicationOrg(tbApplication.getChainId(), tbApplication.getId(), orgId, OrgTypeEnum.FISCO_ORG.getValue());

        //join org to app
        Set<String> nodeIdList = nodeList.stream().map(n -> n.getNodeId()).collect(Collectors.toSet());
        fiscoChainMgrRestService.addSealerAsync(tbApplication.getChainId(), tbApplication.getId(), nodeIdList);
    }


    @Transactional
    public void joinFabricOrg(TbApplication tbApplication, int orgId) {
        //TODO fabric
    }


    @Transactional
    public void removeOrgFromApp(int applicationId, int orgId) {
        //verify appId
        TbApplication tbApplication = applicationManager.verifyApplicationId(applicationId);

        if (ChainTypeEnum.FISCO.getId() == tbApplication.getChainType()) {
            removeFiscoOrg(tbApplication, orgId);
        } else {
            log.warn("not support chainType:{}", tbApplication.getChainType());
            throw new WebServerException(ConstantCode.CHAIN_TYPE_NOT_SUPPORT);
        }
    }


    @Transactional
    public void removeFiscoOrg(TbApplication tbApplication, int orgId) {
        //check app org
        List<TbApplicationOrg> tbApplicationOrgList = applicationOrgMapper.selectByApplicationAndOrg(tbApplication.getId(), orgId);
        if (CollectionUtil.isEmpty(tbApplicationOrgList))
            throw new WebServerException(ConstantCode.ORG_HAS_NOT_JOIN_APPLICATION);

        //query node list by app and org
        String nodeTypes = String.join(",", NODE_TYPE_OBSERVER, NODE_TYPE_SEALER);
        List<RspSimpleNodeInfoVo> nodeList = fiscoChainMgrRestService.selectNodeIdList(tbApplication.getChainId(), tbApplication.getId(), orgId, nodeTypes);
        if (CollectionUtil.isEmpty(nodeList))
            throw new WebServerException(ConstantCode.NODE_FOUND_NODE_OF_OBSERVER_OR_SEALER);

        // save application-org
        applicationOrgMapper.deleteByPrimaryKey(tbApplicationOrgList.get(0).getId());

        //join org to app
        Set<String> nodeIdList = nodeList.stream().map(n -> n.getNodeId()).collect(Collectors.toSet());
        fiscoChainMgrRestService.removeNodeFromGroup(tbApplication.getChainId(), tbApplication.getId(), nodeIdList);
    }

    @Transactional
    public void removeFabricOrg(TbApplication tbApplication, int orgId) {

    }
}
